import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import scale, StandardScaler, MinMaxScaler, normalize, Normalizer, MaxAbsScaler


class Simple(object):
    """
    简单knn
    """
    def __init__(self):
        self.group = np.array([[1.0, 1.1], [1.0, 1.0], [0, 0], [0, 0.1]])
        self.labels = ['A', 'A', 'B', 'B']
        self.test_data = [0, 0]
        self.k = 3

    @staticmethod
    def knn_classfiy0(inx, dataset, labels, k):
        """
        knn算法
        计算指定点与每个点的欧氏距离，排序后获取前k个最小的距离的index，
        对每个对应的label进行计数，返回计数最大的label，即为分类的label
        :param inx:
        :param dataset:
        :param labels:
        :param k:
        :return:
        """
        dataset_size = dataset.shape[0]
        #inx沿x轴复制1倍，沿y轴复制dataset_size倍：计算inx与每个点的距离
        # print(np.tile(inx, (dataset_size, 1)))
        diff_mat = np.tile(inx, (dataset_size, 1)) - dataset
        distances = np.power(np.power(diff_mat, 2).sum(axis=1), 0.5)

        sorteddistanceindeices = distances.argsort()
        # 获得最前面的k个的分类
        class_count = {}
        for i in range(k):
            vote_label = labels[sorteddistanceindeices[i]]
            class_count[vote_label] = class_count.get(vote_label, 0) + 1

        # 对分类结果按类型数量倒序
        sorted_class_count = sorted(class_count.items(), key=lambda d: d[1], reverse=True)
        # print(sorted_class_count)
        return sorted_class_count[0][0]

    @staticmethod
    def knn_sklearn(group, labels, k, test_data):
        """
        knn算法： sklearn
        :return:
        """
        label_dict = {'A': 0, 'B': 1}

        clf = KNeighborsClassifier(n_neighbors=k)
        clf.fit(group, [label_dict.get(x) for x in labels])
        pre_label = clf.predict([test_data])
        pre_label_proba = clf.predict_proba([test_data])

        # return pre_label[0]
        return 'A' if pre_label[0] == 0 else 'B', pre_label_proba

    def knn_main(self):
        """
        主程序入口
        :return:
        """
        # 自主计算的knn
        label_classfiy0 = self.knn_classfiy0(inx=self.test_data, dataset=self.group, labels=self.labels, k=self.k)

        # sklearn的knn
        label_sklearn, label_proba = self.knn_sklearn(self.group, self.labels, self.k, self.test_data)

        info = '自主计算knn的预测label: {}\nsklearn计算knn的预测label: {}\nsklearn计算knn的预测label的概率: {}'.\
            format(label_classfiy0, label_sklearn, label_proba)
        print(info)


class KnnCode(object):
    """
    knn源码
    """
    @staticmethod
    def knn_classfiy0(inx, dataset, labels, k):
        """
        knn算法
        计算指定点与每个点的欧氏距离，排序后获取前k个最小的距离的index，
        对每个对应的label进行计数，返回计数最大的label，即为分类的label
        :param inx:
        :param dataset:
        :param labels:
        :param k:
        :return:
        """
        dataset_size = dataset.shape[0]
        # inx沿x轴复制1倍，沿y轴复制dataset_size倍：计算inx与每个点的距离
        # print(np.tile(inx, (dataset_size, 1)))
        diff_mat = np.tile(inx, (dataset_size, 1)) - dataset
        distances = np.power(np.power(diff_mat, 2).sum(axis=1), 0.5)

        sorteddistanceindeices = distances.argsort()
        # 获得最前面的k个的分类
        class_count = {}
        for i in range(k):
            vote_label = labels[sorteddistanceindeices[i]]
            class_count[vote_label] = class_count.get(vote_label, 0) + 1

        # 对分类结果按类型数量倒序
        sorted_class_count = sorted(class_count.items(), key=lambda d: d[1], reverse=True)
        # print(sorted_class_count)
        return sorted_class_count[0][0]

    @staticmethod
    def file_matrix(filename):
        """
        读取文件，并切换文件内容为x和标签y
        :param filename:
        :return:
        """
        with open(filename) as fr:
            array_lines = fr.readlines()
        num_lines = len(array_lines)
        matrix = np.zeros((num_lines, 3))
        class_label_vector = []
        for index, line in enumerate(array_lines):
            line = line.strip()
            list_line = line.split('\t')
            matrix[index, :] = list_line[: 3]
            class_label_vector.append(int(list_line[-1]))
        return matrix, class_label_vector

    @staticmethod
    def auto_norm(matrix):
        """
        矩阵归一化: (x - min) / (max - min)
        :param x:
        :return:
        """
        min_value = np.array(matrix).min(axis=0)
        ranges = np.array(matrix).max(axis=0) - min_value

        norm_matrix = (np.array(matrix) - min_value) / ranges
        return norm_matrix, range, min_value

    def knn_main(self):
        """
        主程序
        :return:
        """
        hot_ratio = 0.1
        # 读取数据
        matrix, matrix_label = self.file_matrix('./data/machinelearninginaction/Ch02/datingTestSet2.txt')

        # 数据归一化
        matrix_norm, ranges, min_value = self.auto_norm(matrix)

        m = matrix_norm.shape[0]
        num_test = int(m * hot_ratio)
        error_count = 0
        for i in range(num_test):
            classifier_res = self.knn_classfiy0(matrix_norm[i, :], matrix_norm[num_test: m, :],
                                                  matrix_label[num_test: m], 3)
            # print('该类别为： {}，预测为：{}'.format(matrix_label[i], classifier_res))
            if (classifier_res != matrix_label[i]):
                error_count += 1

        print('错误率： {}'.format(error_count / num_test))


class KnnSklearn(object):
    """
    knn sklearn
    """
    @staticmethod
    def file_matrix(filename):
        """
        读取文件，并切换文件内容为x和标签y
        :param filename:
        :return:
        """
        with open(filename) as fr:
            array_lines = fr.readlines()
        num_lines = len(array_lines)
        matrix = np.zeros((num_lines, 3))
        class_label_vector = []
        for index, line in enumerate(array_lines):
            line = line.strip()
            list_line = line.split('\t')
            matrix[index, :] = list_line[: 3]
            class_label_vector.append(int(list_line[-1]))
        return matrix, class_label_vector

    def knn_main(self):
        """

        :return:
        """
        clf = KNeighborsClassifier(n_neighbors=3)

        hot_ratio = 0.1
        # 读取数据
        matrix, matrix_label = self.file_matrix('./data/machinelearninginaction/Ch02/datingTestSet2.txt')

        # 数据归一化
        matrix_norm = MinMaxScaler().fit_transform(matrix)

        m = matrix_norm.shape[0]
        num_test = int(m * hot_ratio)
        error_count = 0
        # 建模
        model = clf.fit(matrix_norm[num_test: m, :], matrix_label[num_test: m])
        for i in range(num_test):
            classifier_res = model.predict([matrix_norm[i, :]])
            # print('该类别为： {}，预测为：{}'.format(matrix_label[i], classifier_res))
            if (classifier_res != matrix_label[i]):
                error_count += 1

        print('错误率： {}'.format(error_count / num_test))


# knn_simple = Simple()
# knn_simple.knn_main()

def plot(matrix, matrix_label):
    """
    画图
    :param matrix:
    :param matrix_label:
    :return:
    """
    # matplotlib中文显示方块
    mpl.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体
    mpl.rcParams['axes.unicode_minus'] = False  # 解决保存图像是负号'-'显示为方块的问题

    # 用label来进行颜色区分
    plt.scatter(matrix[:, 1], matrix[:, 2], 15.0 * np.array(matrix_label), 15.0 * np.array(matrix_label))
    plt.xlabel('玩视频游戏所耗时间百分比')
    plt.ylabel('每周所消费的冰淇淋公升数')

    plt.show()


def sk_norm(matrix):
    """
    sklearn的几种数据标准化/规范化的方法
    :param matrix:
    :return:
    """
    # 1、z-score方法：(x - x.mean) / x.std()
    # a）、scale: 其中可以定义方向，沿x轴（axis=0） or 沿y轴（axis=0）
    # scale_matrix = scale(matrix)
    # print(scale_matrix)

    # b）、StandardScaler
    # standard_scaler = StandardScaler().fit_transform(matrix)
    # print(standard_scaler)

    # 2、最小－最大规范化方法：(x - min) / (max - min)
    # a)、MinMaxScaler
    min_max_scale = MinMaxScaler().fit_transform(matrix)
    # print(min_max_scale)

    # 3、最大规范化方法：x / max：这个方法对那些已经中心化均值维0或者稀疏的数据有意义。
    # max_scale = MaxAbsScaler().fit_transform(matrix)
    # print(max_scale)

    # 4、正则化
    """
    正则化的过程是将每个样本缩放到单位范数（每个样本的范数为1），如果后面要使用如二次型（点积）或者其它核方法计算两个样本之间的相似性这个方法会很有用。
    Normalization主要思想是对每个样本计算其p-范数，然后对该样本中每个元素除以该范数，这样处理的结果是使得每个处理后样本的p-范数（l1-norm,l2-norm）等于1。
        p-范数的计算公式：||X||p=(|x1|^p+|x2|^p+...+|xn|^p)^1/p
    该方法主要应用于文本分类和聚类中。例如，对于两个TF-IDF向量的l2-norm进行点积，就可以得到这两个向量的余弦相似性。
    """
    #  对指定数据进行转换
    # normal_matri = normalize(matrix, norm='l2')
    # print(normal_matri)
    # 对训练集和测试机的拟合和转换
    # normal_matri = Normalizer().fit_transform(matrix)
    # print(normal_matri)

    # 5、其它情况
    """
      1)、规模化的数据有很多异常值，无法使用数据规范化，此时可以使用robust_scale 和 RobustScale，
            它们会根据中位数或者四分位数去中心化数据
      2）、二值化：Binarizer：默认是根据0来二值化（可以自定义阈值threshold），大于0的都标记为1，小于等于0的都标记为0
      3）、类别特征编码：若粗暴的将性别，国家等特征手动编码为数字，sklearn会把它们当成序列的连续变量。
            要想使得类别型的变量能最终被模型直接使用，可以使用one-of-k编码或者one-hot编码。
            这些都可以通过OneHotEncoder/OrdinalEncoder实现，它可以将有n种值的一个特征变成n个二元的特征
    
    """


# 0.05
# knn_code = KnnCode()
# knn_code.knn_main()

knn_sklearn = KnnSklearn()
knn_sklearn.knn_main()
